#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <openssl/sha.h>
#include <curl/curl.h>
#include "thirdparty/cJSON.h"
#include "thirdparty/log.h"
#include "ota/ota_http.h"
#include "ota/ota_memchunk.h"

//在获取到响应后 后台会调用这个函数把数据写入到可变字符串
// contents  响应的数据
// size      一次写入的数据长度（字节）
// nmemb     一共多所个 这种size  
// userp     写到这个字符串
//失败返回 -1  成功返回长度
static size_t ota_http_writeChunkCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
    MemChunk *chunk = userp;
    //写入数据
    if(ota_memchunk_append(chunk,contents,size * nmemb) < 0)
    {
        log_warn("ota_http_writeChunkCallback write fial");
        return -1;
    }
    return size * nmemb;
}

/// @brief 获取filename文件里面的固件的哈希至
/// @param filename 文件
/// @param digest 存储固件的哈希值
/// @return 
static int ota_http_calculateSHA1(char *filename, char *digest)
{
    //定义一个buf
    unsigned char buf[1024];  //一次从文件度1024个字节
    //存储2进制的本地校验马
    unsigned char sha_digest[SHA_DIGEST_LENGTH];
    //使用2进制的形式打开一个文件不做仍和修改  普通打开\n 的二进制数据都会表变成环行，我们要原始数据
    FILE *file = fopen(filename,"rb");
    //判断打开出没有
    if(file == NULL)
    {
        //失败
        log_warn("ota_http_calculateSHA1 open file fail");
        return -1;
    }
    //初始化一个  sha（一种哈希算法）句柄
    SHA_CTX ctx; 
    int result = 0;  //用来判断下面的从操作有时失败没有 成功都是返回0
    int len = 0; //储存一次冲文件中读取的长度
    //初始化sha句柄
    result |= SHA1_Init(&ctx);  //成功返回0
    //把文件里面的数据循环读到buf中  在把buf中的数据更新到cha句柄中
    while((len = fwrite(buf,sizeof(buf),1,file)) != 0)
    {
        //len的结构不等于0  表示读取成功了  把buf里面的数据更新到 sha句柄
        result |= SHA1_Update(&ctx,buf,len); //成功返回0
    }
    //计算哈希值  因为还是2禁止格式  村到sha_digest
    result |= SHA1_Final(sha_digest, &ctx);
    //判断有没有失败
    if(result != 0)
    {
        //失败
        fclose(file);
        log_warn("ota_http_calculateSHA1 Final fail");
        return -1;
    }
    //关闭文件
    fclose(file);
    //把计算的哈希值从十六进制格式转换成文本格式  比如   一个字节 1A  转成2个字符 “1A”
    for(int i = 0;i < SHA_DIGEST_LENGTH;i++)
    {
        //指定输出一个十六进制数，并确保输出至少有两个字符。如果数值小于两位，则会在前面补零
        sprintf(digest + i * 2, "%02X", sha_digest[i]);
    }
    log_trace("ota_http_calculateSHA1 succeed");
    return 0;
}

/**
 * @brief 请求url，并将结果存放到chunk中
 *
 * @param url 请求网址
 * @param chunk 内存块
 * @return int 0成功 -1失败
 */
static int ota_http_requestToChunk(char *url, MemChunk *chunk)
{
    //创建一个 curl句柄
    CURL *curl;
    //初始化句柄
    curl = curl_easy_init();
    if(curl == NULL)
    {
        //失败
        log_warn("ota_http_requestToChunk curl_easy_init fial");
        return -1;
    }
    //设置要访问的网站
    curl_easy_setopt(curl, CURLOPT_URL, url);
    //设置执行写入文件的回调函数 他默认是写入文件所以他的回调函数格式和 fwrite一样  我们封装一个
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, ota_http_writeChunkCallback);
    //设置具体写入那个文件
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, chunk);
    //发送 请求
    int result = curl_easy_perform(curl);
    //判断是否成功
    if(result != CURLE_OK)
    {
        //失败
        log_warn("ota_http_requestToChunk curl_easy_perform fial");
        curl_easy_cleanup(curl);
        return -1;
    }
    //获取响应文件中响应马 来判断这一次给你回应的响应是否能够使用
    long http_code;
    //获取响应中的响应马
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    //只要响应码不为 200  说明文件（chunk）里面的不能用
    if(http_code != 200)
    {
        //失败
        log_warn("ota_http_requestToChunk nfo erreo");
        curl_easy_cleanup(curl);
        return -1;
    }
    //删除 curl句柄
    curl_easy_cleanup(curl);
    log_trace("ota_http_requestToChunk succeed");
    return 0;
}


/// @brief 下载固件到文件
/// @param url 网址
/// @param file 存储文件
/// @return 
static int ota_http_requestToFile(char *url, FILE *file)
{
    //创建一个 curl句柄
    CURL *curl;
    //初始化句柄
    curl = curl_easy_init();
    if(curl == NULL)
    {
        //失败
        log_warn("ota_http_requestToFile curl_easy_init fial");
        return -1;
    }
    //设置要访问的网站
    curl_easy_setopt(curl, CURLOPT_URL, url);
    //设置执行写入文件的回调函数 他默认的就是fwrite 我们直接用不用再次实现
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
    //设置具体写入那个文件
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
    //发送 请求
    int result = curl_easy_perform(curl);
    //判断是否成功
    if(result != CURLE_OK)
    {
        //失败
        log_warn("ota_http_requestToFile curl_easy_perform fial");
        curl_easy_cleanup(curl);
        return -1;
    }
    //获取响应文件中响应马 来判断这一次给你回应的响应是否能够使用
    long http_code;
    //获取响应中的响应马
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    //只要响应码不为 200  说明文件（chunk）里面的不能用
    if(http_code != 200)
    {
        //失败
        log_warn("ota_http_requestToFile nfo erreo");
        curl_easy_cleanup(curl);
        return -1;
    }
    //删除 curl句柄
    curl_easy_cleanup(curl);
    log_trace("ota_http_requestToFile succeed");
    return 0;
}


/// @brief 下载新的版本
/// @param url ip + 访问资源
/// @param version  存储版本号
/// @return 
int ota_http_getVersion(char *url, Version *version)
{
    //判断结构体是否定义
    if(version == NULL)
    {
        //为定义
        log_warn("ota_http_getVersion version not define");
        return -1;
    }
    //初始化一个chunk
    MemChunk chunk;
    memset(&chunk,0,sizeof(MemChunk));
    if(ota_memchunk_init(&chunk) < 0)
    {
        //失败
        log_warn("ota_http_getVersion version init fial");
        return -1;
    }
    //获取服务器上面的版本号存入chunk
    if(ota_http_requestToChunk(url,&chunk) < 0)
    {
        //失败
        log_warn("ota_http_requestToChunk fail");
        ota_memchunk_free(&chunk);
        return -1;
    }
    //解析json
    cJSON *obj = cJSON_Parse(chunk.data);
    //获取建值对
    cJSON *major = cJSON_GetObjectItem(obj,"major");
    cJSON *minor = cJSON_GetObjectItem(obj,"minor");
    cJSON *patch = cJSON_GetObjectItem(obj,"patch");
    //判断获取成功没有
    if(major == NULL || minor == NULL || patch ==NULL)
    {
        //只要一个没有分配出  就失败
        log_warn("ota_http_getVersion GetObjectItem fial");
        ota_memchunk_free(&chunk);
        cJSON_Delete(obj);
        return -1;
    }
    //获取版本号
    version->major = major->valueint;
    version->minor = minor->valueint;
    version->patch = patch->valueint;
    log_trace("ota_http_getVersion seccuud");
    return 0;
}


/// @brief 下载固件和摘要
/// @param firmware_url 下载固件的 ip + 访问资源
/// @param digest_url 下载摘要的 ip + 访问资源
/// @param file 存储固件的文件
/// @return 
int ota_http_downloadFirmware(char *firmware_url, char *digest_url, char *file)
{
    //打开文件 用来存放下载的固件
    FILE *firmware = fopen(file,"w");
    if(firmware  == NULL)
    {
        //失败
        return -1;
    }
    //下载固件到文件
    if(ota_http_requestToFile(firmware_url,firmware) < 0)
    {
        //失败
        fclose(firmware);   //关闭文件
        return -1;
    }
    //关闭文件
    fclose(firmware); 
    //创建一个 chunk 存放 摘要（固件的哈希置）
    MemChunk chunk;
    memset(&chunk,0,sizeof(MemChunk));  //把里面清空
    ota_memchunk_init(&chunk);          //初始化chunk
    //从服务器获取 固件的摘要（固件的哈希置）
    if(ota_http_requestToChunk(digest_url,&chunk) < 0)
    {
        log_warn("ota_http_downloadFirmware ota_http_requestToChunk fail");
        //释放chunk
        ota_memchunk_free(&chunk);
        return -1;
    }
    //获取计算刚刚下载的固件的摘要（固件的哈希置）
    char buf[SHA_DIGEST_LENGTH * 2 + 1]; //存放转为文本格式的刚刚下载的固件的摘要（固件的哈希置）
    if(ota_http_calculateSHA1(file,buf) < 0)
    {
        log_warn("ota_http_downloadFirmware ota_http_calculateSHA1 fail");
        //释放chunk
        ota_memchunk_free(&chunk);
        return -1;
    }
    //把刚刚下载的固件哈希值 和 服务器上面的固件哈希值对比 一样表示固件下载成功 否则失败
    if(memcmp(chunk.data,buf,SHA_DIGEST_LENGTH * 2) != 0)
    {
        log_warn("ota_http_downloadFirmware memcmp fail");
        //失败
        ota_memchunk_free(&chunk);
        return -1;
    }
    ota_memchunk_free(&chunk);
    log_trace("ota_http_downloadFirmware succeed");
    return 0;
}